use diesel::{
    dsl::{Filter, Find, Limit, Offset, Order, Select},
    prelude::*,
    query_dsl::methods::{FilterDsl, FindDsl, LimitDsl, LoadQuery, OffsetDsl, OrderDsl, SelectDsl},
};

use crate::database::DbError;

pub fn read<'a, T, M, ColExpr, PK>(
    conn: &mut MysqlConnection,
    table: T,
    cols: ColExpr,
    id: PK,
) -> Result<M, DbError>
where
    T: FindDsl<PK>,
    Find<T, PK>: SelectDsl<ColExpr>,
    Select<Find<T, PK>, ColExpr>: LimitDsl,
    Limit<Select<Find<T, PK>, ColExpr>>: LoadQuery<'a, MysqlConnection, M>,
    ColExpr: Expression,
{
    conn.transaction(|conn| {
        let item = table.find(id).select(cols).limit(1).get_result(conn)?;
        Ok(item)
    })
}

pub fn read_by_field<'a, T, M, ColExpr, FilterExpr>(
    conn: &mut MysqlConnection,
    table: T,
    cols: ColExpr,
    filter: FilterExpr,
) -> Result<M, DbError>
where
    T: FilterDsl<FilterExpr>,
    Filter<T, FilterExpr>: SelectDsl<ColExpr>,
    Select<Filter<T, FilterExpr>, ColExpr>: LimitDsl,
    Limit<Select<Filter<T, FilterExpr>, ColExpr>>: LoadQuery<'a, MysqlConnection, M>,
    ColExpr: Expression,
{
    conn.transaction(|conn| {
        let items = table
            .filter(filter)
            .select(cols)
            .limit(1)
            .get_result(conn)?;
        Ok(items)
    })
}

pub fn reads_offset<'a, T, M, OrderExpr, ColExpr>(
    conn: &mut MysqlConnection,
    table: T,
    cols: ColExpr,
    order: OrderExpr,
    count: i64,
    offset: i64,
) -> Result<Vec<M>, DbError>
where
    T: Table + OrderDsl<OrderExpr>,
    Order<T, OrderExpr>: SelectDsl<ColExpr>,
    Select<Order<T, OrderExpr>, ColExpr>: LimitDsl,
    Limit<Select<Order<T, OrderExpr>, ColExpr>>: OffsetDsl,
    Offset<Limit<Select<Order<T, OrderExpr>, ColExpr>>>: LoadQuery<'a, MysqlConnection, M>,
    OrderExpr: Expression,
    ColExpr: Expression,
{
    conn.transaction(|conn| {
        let items = table
            .order(order)
            .select(cols)
            .limit(count)
            .offset(offset)
            .load(conn)?;
        Ok(items)
    })
}

pub fn reads_offset_by_field<'a, T, M, ColExpr, FilterExpr, OrderExpr>(
    conn: &mut MysqlConnection,
    table: T,
    cols: ColExpr,
    filter: FilterExpr,
    order: OrderExpr,
    count: i64,
    offset: i64,
) -> Result<Vec<M>, DbError>
where
    T: FilterDsl<FilterExpr>,
    Filter<T, FilterExpr>: OrderDsl<OrderExpr>,
    Order<Filter<T, FilterExpr>, OrderExpr>: SelectDsl<ColExpr>,
    Select<Order<Filter<T, FilterExpr>, OrderExpr>, ColExpr>: LimitDsl,
    Limit<Select<Order<Filter<T, FilterExpr>, OrderExpr>, ColExpr>>: OffsetDsl,
    Offset<Limit<Select<Order<Filter<T, FilterExpr>, OrderExpr>, ColExpr>>>:
        LoadQuery<'a, MysqlConnection, M>,
    ColExpr: Expression,
    OrderExpr: Expression,
{
    conn.transaction(|conn| {
        let items = table
            .filter(filter)
            .order(order)
            .select(cols)
            .limit(count)
            .offset(offset)
            .load(conn)?;
        Ok(items)
    })
}

#[macro_export]
macro_rules! db_read {
    ($table:ident, $model:ident, $fn:ident) => {
        pub fn $fn(conn: &mut MysqlConnection, id: i32) -> Result<$model, DbError> {
            let selected_item =
                crud::read::read::<_, $model, _, _>(conn, $table::table, $model::as_select(), id)?;
            Ok(selected_item)
        }
    };
}

#[macro_export]
macro_rules! db_read_id_in {
    ($table:ident, $model:ident, $fn:ident) => {
        pub fn $fn(conn: &mut MysqlConnection, ids: Vec<i32>) -> Result<Vec<$model>, DbError> {
            let select_rows = crud::read::reads_offset_by_field(
                conn,
                $table::table,
                $model::as_select(),
                $table::id.eq_any(&ids),
                $table::id.desc(),
                ids.len() as i64,
                0,
            )?;
            Ok(select_rows)
        }
    };
}

#[macro_export]
macro_rules! db_read_by_field {
    ($table:ident, $model:ident, $fn:ident, $field:ident, $type:ident) => {
        pub fn $fn(conn: &mut MysqlConnection, $field: $type) -> Result<$model, DbError> {
            let selected_item = crud::read::read_by_field(
                conn,
                $table::table,
                $model::as_select(),
                $table::$field.eq($field),
            )?;
            Ok(selected_item)
        }
    };
}

#[macro_export]
macro_rules! db_read_count_from_offset {
    ($table:ident, $model:ident, $fn:ident) => {
        pub fn $fn(
            conn: &mut MysqlConnection,
            count: i64,
            offset: i64,
        ) -> Result<Vec<$model>, DbError> {
            let selected_items = crud::read::reads_offset(
                conn,
                $table::table,
                $model::as_select(),
                $table::id.desc(),
                count,
                offset,
            )?;
            Ok(selected_items)
        }
    };
}

#[macro_export]
macro_rules! db_read_count_from_offset_by_field {
    ($table:ident, $model:ident, $fn:ident, $field:ident, $type:ident) => {
        pub fn $fn(
            conn: &mut MysqlConnection,
            $field: $type,
            count: i64,
            offset: i64,
        ) -> Result<Vec<$model>, DbError> {
            let select_rows = crud::read::reads_offset_by_field(
                conn,
                $table::table,
                $model::as_select(),
                $table::$field.eq($field),
                $table::id.desc(),
                count,
                offset,
            )?;
            Ok(select_rows)
        }
    };
}

#[macro_export]
macro_rules! test_db_read {
    () => {
        #[test]
        fn test_read_id() {
            let mut conn = establish_connection();
            conn.test_transaction::<_, diesel::result::Error, _>(|conn| {
                let (added_id, _, _) = insert_test_data(conn);
                let added_item = read(conn, added_id).unwrap();
                assert!(added_item.id == added_id);
                Ok(())
            });
        }
    };
}

#[macro_export]
macro_rules! test_db_reads_offset {
    () => {
        #[test]
        fn test_read() {
            let mut conn = establish_connection();
            conn.test_transaction::<_, diesel::result::Error, _>(|conn| {
                let (_, _, _) = insert_test_data(conn);
                let selected_items = reads_offset(conn, 8, 0).unwrap();
                assert!(selected_items.len() >= 2);
                Ok(())
            });
        }
    };
}

#[macro_export]
macro_rules! test_db_reads_by_field {
    ($test_fn:ident, $fn:ident, $no:expr) => {
        #[test]
        fn $test_fn() {
            let mut conn = establish_connection();
            conn.test_transaction::<_, diesel::result::Error, _>(|conn| {
                let (_, fid1, fid2) = insert_test_data(conn);
                let items = if $no == 1 {
                    $fn(conn, fid1, 8, 0).unwrap()
                } else {
                    $fn(conn, fid2, 8, 0).unwrap()
                };
                assert!(items.len() >= 2);
                Ok(())
            });
        }
    };
}
